Skip to content

Automated Test: oauth-state-secure #312

Closed
wants to merge 1 commit into from

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features

    • Added OAuth-based login authentication for GitHub integration, enabling a more streamlined and secure setup experience
    • Improved error handling during installation with clearer, more helpful failure messages to guide users through issues
  • Improvements

    • Enhanced installation pipeline to better support multi-step setup workflows and complex organizational scenarios

…#67876)

We're adding one more step in the GitHub integration installation
pipeline, namely GitHub OAuth2 authorize. This is transparent from the
UX perspective as the data exchange happens without user interaction.

The pipeline will now fail in these cases:
- If there is a mismatch between currently authenticated GitHub user
(derived from OAuth2 authorize step) and the user who installed the
GitHub app (https://github.com/apps/sentry-io)
- If there is a mismatch between `state` parameter supplied by user and
pipeline signature
- If GitHub could not generate correct `access_token` from the `code`
(wrong or attempt of re-use of `code`).

In all those cases, this error is shown:

![image](https://github.com/getsentry/sentry/assets/1127549/18923861-2ead-4cf5-adda-7738aef801d7)
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

GitHub integration now supports OAuth-based login flow during installation setup. New error handling infrastructure and helper functions provide standardized failure page rendering. The pipeline is updated to include an OAuthLoginView alongside existing installation views. A simplification in pipeline advancer removes a list constant for direct GitHub provider comparison.

Changes

Cohort / File(s) Summary
GitHub Integration OAuth Flow
src/sentry/integrations/github/integration.py
Added OAuth-based authentication with new OAuthLoginView class implementing GitHub authorization flow, state validation, token exchange, and user persistence. Introduced error handling with error() and get_document_origin() helper functions plus ERR_INTEGRATION_INVALID_INSTALLATION_REQUEST constant. Updated GitHubIntegrationProvider.get_pipeline_views() to include OAuth view.
Pipeline Advancer Simplification
src/sentry/web/frontend/pipeline_advancer.py
Removed module-level FORWARD_INSTALL_FOR constant and replaced list membership check with direct GitHub provider comparison (provider_id == 'github'). Added clarifying comment on redirect behavior.
GitHub Integration Test Expansion
tests/sentry/integrations/github/test_integration.py
Added OAuth token and user endpoint stubs. Modified assert_setup_flow() helper to return response object. Introduced test_github_user_mismatch() test for mismatched installation scenarios. Extended setup flow assertions to verify OAuth redirect URLs and query parameters.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Sentry as Sentry<br/>(OAuthLoginView)
    participant GitHub as GitHub<br/>OAuth
    participant Pipeline as Pipeline<br/>State

    Client->>Sentry: Initiate GitHub setup
    Sentry->>GitHub: Redirect to authorize<br/>(client_id, state, scope)
    GitHub->>Client: Authorization prompt
    Client->>GitHub: User grants permission
    GitHub->>Sentry: Callback with code & state
    Sentry->>Sentry: Validate state parameter
    Sentry->>GitHub: Exchange code for token
    GitHub->>Sentry: Access token response
    Sentry->>GitHub: Fetch user info
    GitHub->>Sentry: User data (login, etc.)
    Sentry->>Pipeline: Persist authenticated<br/>user & token
    Sentry->>Client: Proceed to next<br/>pipeline view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through OAuth's gate,
Where GitHub flows and tokens mate!
New views and error paths align,
The integration starts to shine—
Login dances, states validate,
What once took many, now feels great! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: oauth-state-secure' is vague and generic, using non-descriptive terms that don't clearly convey what changes were made to the codebase. Provide a more descriptive title that summarizes the primary code changes, such as 'Add OAuth login view to GitHub integration' or 'Refactor GitHub OAuth setup flow with state validation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch oauth-state-secure

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/sentry/integrations/github/test_integration.py (1)

378-396: ⚠️ Potential issue | 🟡 Minor

Test name test_installation_not_found is now misleading — it tests OAuth state mismatch instead.

The 404 stub for the installation endpoint (Line 381–383) is never reached because the OAuth state "ddd023d87a913d5226e2a882c4c4cc05" doesn't match the pipeline signature, causing OAuthLoginView to reject the request before the installation lookup. The test now validates state mismatch, not a 404 on the installation. Consider renaming it (e.g., test_invalid_oauth_state) or adding a separate test that exercises the actual installation-not-found path through a valid OAuth flow.

🤖 Fix all issues with AI agents
In `@src/sentry/integrations/github/integration.py`:
- Around line 493-505: The code assumes integration.metadata["sender"] always
exists and will raise KeyError for older integrations; update the check that
compares pipeline.fetch_state("github_authenticated_user") with
integration.metadata["sender"]["login"] to safely handle missing metadata by
using dict.get (e.g. integration.metadata.get("sender", {}) or retrieving sender
= integration.metadata.get("sender")) or by catching KeyError, and if sender or
sender["login"] is absent treat it as a mismatch and return error(request,
self.active_organization); ensure you reference Integration and
pipeline.fetch_state("github_authenticated_user") when making the change so the
comparison remains robust.
- Around line 447-454: The dispatch method now only checks "if installation_id
is None" which lets empty strings through and binds them via
pipeline.bind_state, causing downstream get_installation_info calls to hit an
invalid /app/installations/ endpoint; change the check to reject empty strings
(e.g., use a truthy check like "if not installation_id" or explicitly validate
installation_id.strip() is non-empty) before redirecting, ensuring
pipeline.fetch_state/pipeline.bind_state only see a valid non-empty
installation_id used by get_installation_info.

In `@tests/sentry/integrations/github/test_integration.py`:
- Around line 398-454: The test test_github_user_mismatch fails because the
hardcoded HTTP_X_HUB_SIGNATURE value does not match the HMAC-SHA1 of the webhook
payload (INSTALLATION_EVENT_EXAMPLE) given the default empty webhook secret,
causing the webhook to be rejected before the user-mismatch logic runs; to fix,
either set the webhook secret explicitly via
options.set("github-app.webhook-secret", "<secret>") in the test setup and use
the matching signature header, or compute the correct HMAC-SHA1 for
INSTALLATION_EVENT_EXAMPLE with the secret used and replace the
HTTP_X_HUB_SIGNATURE header in the client.post call so the signature validation
in the GitHub webhook endpoint passes and the test exercises
GitHubIntegrationTest::test_github_user_mismatch logic.
🧹 Nitpick comments (2)
src/sentry/integrations/github/integration.py (2)

401-409: OAuth authorize URL is constructed via string concatenation without URL-encoding.

While the values (github_client_id, state, redirect_uri) are likely safe in practice (alphanumeric or clean URL paths), query parameter values should be properly URL-encoded to prevent breakage with unexpected characters.

♻️ Suggested fix using `urlencode`
+from urllib.parse import urlencode
...
         if not request.GET.get("state"):
             state = pipeline.signature
 
             redirect_uri = absolute_uri(
                 reverse("sentry-extension-setup", kwargs={"provider_id": "github"})
             )
+            params = urlencode({
+                "client_id": github_client_id,
+                "state": state,
+                "redirect_uri": redirect_uri,
+            })
             return self.redirect(
-                f"{ghip.get_oauth_authorize_url()}?client_id={github_client_id}&state={state}&redirect_uri={redirect_uri}"
+                f"{ghip.get_oauth_authorize_url()}?{params}"
             )

Note: urlencode is already imported at line 7 as parse_qsl — just add urlencode to that import.


425-429: Bare except Exception swallows all errors during token parsing.

Per the Ruff hint (BLE001), catching the broad Exception here could mask unexpected errors (e.g., encoding issues, attribute errors). The likely failure modes are network/parsing errors, so a narrower catch would be safer.

♻️ Suggested narrower exception handling
         try:
             body = safe_urlread(req).decode("utf-8")
             payload = dict(parse_qsl(body))
-        except Exception:
+        except (ValueError, AttributeError, UnicodeDecodeError):
             payload = {}

Comment on lines 447 to +454
def dispatch(self, request: Request, pipeline: Pipeline) -> HttpResponse:
if "installation_id" not in request.GET:
installation_id = request.GET.get(
"installation_id", pipeline.fetch_state("installation_id")
)
if installation_id is None:
return self.redirect(self.get_app_url())

pipeline.bind_state("installation_id", installation_id)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how installation_id is used downstream after being bound to state
rg -n "installation_id" src/sentry/integrations/github/integration.py -C2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 1895


🏁 Script executed:

#!/bin/bash
# Look at build_integration method to see if there's validation handling
sed -n '351,380p' src/sentry/integrations/github/integration.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 1371


Changed from if not installation_id to if installation_id is None — this allows empty strings to proceed.

request.GET.get("installation_id") returns None when absent but "" when present but empty. The old check (if not installation_id) rejected both None and "", while the new check only rejects None. An empty string would now:

  1. Bypass the redirect at line 451-452
  2. Bind an empty string to pipeline state (line 454)
  3. Reach get_installation_info() with an empty string, creating an invalid API call to /app/installations/ (line 348)

This is inconsistent with the pattern used earlier at line 398 (if installation_id:) and creates a behavioral regression. Either revert to the truthy check or explicitly validate that installation_id is non-empty.

🤖 Prompt for AI Agents
In `@src/sentry/integrations/github/integration.py` around lines 447 - 454, The
dispatch method now only checks "if installation_id is None" which lets empty
strings through and binds them via pipeline.bind_state, causing downstream
get_installation_info calls to hit an invalid /app/installations/ endpoint;
change the check to reject empty strings (e.g., use a truthy check like "if not
installation_id" or explicitly validate installation_id.strip() is non-empty)
before redirecting, ensuring pipeline.fetch_state/pipeline.bind_state only see a
valid non-empty installation_id used by get_installation_info.

Comment on lines +493 to +505
try:
integration = Integration.objects.get(
external_id=installation_id, status=ObjectStatus.ACTIVE
)
except Integration.DoesNotExist:
return error(request, self.active_organization)

# Check that the authenticated GitHub user is the same as who installed the app.
if (
pipeline.fetch_state("github_authenticated_user")
!= integration.metadata["sender"]["login"]
):
return error(request, self.active_organization)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

integration.metadata["sender"] will KeyError if the integration was created before this change or via a path that doesn't set "sender".

build_integration only sets metadata["sender"] conditionally (Line 376–377: if state.get("sender")). Pre-existing integrations or those created through flows that don't populate sender in state will lack this key, causing a KeyError at Line 503.

🐛 Proposed fix — guard against missing "sender" metadata
         # Check that the authenticated GitHub user is the same as who installed the app.
-        if (
-            pipeline.fetch_state("github_authenticated_user")
-            != integration.metadata["sender"]["login"]
-        ):
+        sender = integration.metadata.get("sender")
+        if not sender or (
+            pipeline.fetch_state("github_authenticated_user")
+            != sender.get("login")
+        ):
             return error(request, self.active_organization)
🤖 Prompt for AI Agents
In `@src/sentry/integrations/github/integration.py` around lines 493 - 505, The
code assumes integration.metadata["sender"] always exists and will raise
KeyError for older integrations; update the check that compares
pipeline.fetch_state("github_authenticated_user") with
integration.metadata["sender"]["login"] to safely handle missing metadata by
using dict.get (e.g. integration.metadata.get("sender", {}) or retrieving sender
= integration.metadata.get("sender")) or by catching KeyError, and if sender or
sender["login"] is absent treat it as a mismatch and return error(request,
self.active_organization); ensure you reference Integration and
pipeline.fetch_state("github_authenticated_user") when making the change so the
comparison remains robust.

Comment on lines +398 to +454
@responses.activate
def test_github_user_mismatch(self):
self._stub_github()

# Emulate GitHub installation
init_path_1 = "{}?{}".format(
reverse(
"sentry-organization-integrations-setup",
kwargs={
"organization_slug": self.organization.slug,
"provider_id": self.provider.key,
},
),
urlencode({"installation_id": self.installation_id}),
)
self.client.get(init_path_1)

webhook_event = json.loads(INSTALLATION_EVENT_EXAMPLE)
webhook_event["installation"]["id"] = self.installation_id
webhook_event["sender"]["login"] = "attacker"
resp = self.client.post(
path="/extensions/github/webhook/",
data=json.dumps(webhook_event),
content_type="application/json",
HTTP_X_GITHUB_EVENT="installation",
HTTP_X_HUB_SIGNATURE="sha1=d184e6717f8bfbcc291ebc8c0756ee446c6c9486",
HTTP_X_GITHUB_DELIVERY="00000000-0000-4000-8000-1234567890ab",
)
assert resp.status_code == 204

# Validate the installation user
user_2 = self.create_user("foo@example.com")
org_2 = self.create_organization(name="Rowdy Tiger", owner=user_2)
self.login_as(user_2)
init_path_2 = "{}?{}".format(
reverse(
"sentry-organization-integrations-setup",
kwargs={
"organization_slug": org_2.slug,
"provider_id": self.provider.key,
},
),
urlencode({"installation_id": self.installation_id}),
)
setup_path_2 = "{}?{}".format(
self.setup_path,
urlencode(
{"code": "12345678901234567890", "state": "9cae5e88803f35ed7970fc131e6e65d3"}
),
)
with self.feature({"organizations:customer-domains": [org_2.slug]}):
resp = self.client.get(init_path_2)
resp = self.client.get(setup_path_2)
self.assertTemplateUsed(resp, "sentry/integrations/github-integration-failed.html")
assert resp.status_code == 200
assert b'window.opener.postMessage({"success":false' in resp.content
assert b"Invalid installation request." in resp.content

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the webhook secret configuration used in tests
rg -n "webhook.secret\|GITHUB_WEBHOOK_SECRET\|github-app.webhook-secret" --type=py -C2 -g '!node_modules/**'

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look for the test class setup and webhook configuration
cd tests/sentry/integrations/github
rg -n "_stub_github\|INSTALLATION_EVENT_EXAMPLE\|webhook" test_integration.py -B2 -A5 | head -100

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Find where the test fixture sets the webhook secret
rg -n "webhook.*secret\|secret.*webhook" tests/sentry/integrations/github/test_integration.py -i -B3 -A3

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Get the test file to understand the structure
wc -l tests/sentry/integrations/github/test_integration.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 142


🏁 Script executed:

#!/bin/bash
# Read the test class and its setup methods
head -100 tests/sentry/integrations/github/test_integration.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 3662


🏁 Script executed:

#!/bin/bash
# Search for where INSTALLATION_EVENT_EXAMPLE is defined
rg "INSTALLATION_EVENT_EXAMPLE" tests/sentry/integrations/github/test_integration.py -B3 -A3

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 592


🏁 Script executed:

#!/bin/bash
# Look for class definition and any setup/fixture methods
rg "^class |def setUp|def setup|@pytest.fixture" tests/sentry/integrations/github/test_integration.py -n | head -20

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 244


🏁 Script executed:

#!/bin/bash
# Find the fixtures.github file
find . -name "fixtures.py" -o -name "github.py" | grep fixtures

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Search for _stub_github method implementation
rg "_stub_github" tests/sentry/integrations/github/test_integration.py -A 30

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 7260


🏁 Script executed:

#!/bin/bash
# Look for webhook secret in the entire test directory
rg "webhook.*secret\|HUB_SIGNATURE\|d184e6717f8bfbcc291ebc8c0756ee446c6c9486" --type=py tests/sentry/integrations/github/ -B2 -A2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Read the fixtures/github.py file to understand INSTALLATION_EVENT_EXAMPLE
cat fixtures/github.py | head -100

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 3103


🏁 Script executed:

#!/bin/bash
# Find the webhook endpoint handler
rg "extensions/github/webhook" --type=py -B5 -A10

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 50397


🏁 Script executed:

#!/bin/bash
# Look for webhook secret or signature validation in the GitHub integration
rg "X_HUB_SIGNATURE\|webhook.*secret\|verify.*signature" src/sentry/integrations/github/ --type=py -B2 -A5

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Check the entire GitHubIntegrationTest class for webhook secret setup
sed -n '85,1112p' tests/sentry/integrations/github/test_integration.py | grep -n "webhook.*secret\|github-app\|options.set"

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Find and read INSTALLATION_EVENT_EXAMPLE from fixtures
rg "INSTALLATION_EVENT_EXAMPLE.*=" fixtures/github.py -A 80 | head -100

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 3902


🏁 Script executed:

#!/bin/bash
# Check if there's a default webhook secret value
rg "github-app.webhook-secret" src/ --type=py -B2 -A2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 979


🏁 Script executed:

#!/bin/bash
# Calculate the HMAC signature for the webhook event to verify correctness
python3 << 'EOF'
import json
import hmac
import hashlib

# The INSTALLATION_EVENT_EXAMPLE from fixtures
event_json = """{
  "action": "created",
  "installation": {
    "id": 2,
    "account": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "access_tokens_url": "https://api.github.com/installations/2/access_tokens",
    "repositories_url": "https://api.github.com/installation/repositories"
  },
  "sender": {
    "login": "attacker",
    "id": 1,
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    "url": "https://api.github.com/users/octocat",
    "html_url": "https://github.com/octocat",
    "followers_url": "https://api.github.com/users/octocat/followers",
    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
    "organizations_url": "https://api.github.com/users/octocat/orgs",
    "repos_url": "https://api.github.com/users/octocat/repos",
    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
    "received_events_url": "https://api.github.com/users/octocat/received_events",
    "type": "User",
    "site_admin": false
  }
}"""

# Test with default empty secret
secret = ""
payload = event_json.encode('utf-8')
signature = hmac.new(secret.encode('utf-8'), payload, hashlib.sha1).hexdigest()

print(f"Calculated signature (empty secret): sha1={signature}")
print(f"Hardcoded signature in test:        sha1=d184e6717f8bfbcc291ebc8c0756ee446c6c9486")
print(f"Match: {signature == 'd184e6717f8bfbcc291ebc8c0756ee446c6c9486'}")

# Also test with the secret from test_webhooks
secret2 = "b3002c3e321d4b7880360d397db2ccfd"
signature2 = hmac.new(secret2.encode('utf-8'), payload, hashlib.sha1).hexdigest()
print(f"\nCalculated signature (b3002c3e secret): sha1={signature2}")
EOF

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 349


🏁 Script executed:

#!/bin/bash
# Check if GitHubIntegrationTest class has any setUp or fixture that sets webhook secret
head -300 tests/sentry/integrations/github/test_integration.py | tail -150

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 6169


🏁 Script executed:

#!/bin/bash
# Check how the webhook endpoint validates signatures
rg "X_HUB_SIGNATURE\|verify.*signature" src/sentry/integrations/github/webhook.py -B5 -A10

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look at the dispatch/post method in the webhook handler
sed -n '1,100p' src/sentry/integrations/github/webhook.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 3963


🏁 Script executed:

#!/bin/bash
# Continue reading the webhook file to see signature verification
sed -n '100,300p' src/sentry/integrations/github/webhook.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 7692


🏁 Script executed:

#!/bin/bash
# Find the Endpoint class that handles the actual webhook POST
rg "class.*Endpoint.*webhook\|def post.*Request" src/sentry/integrations/github/webhook.py -A20 | head -80

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/antml:thinking>
Let me search for the GitHubInstallation class and dispatch method mentioned in the review
</thinking>
#!/bin/bash
# Search for GitHubInstallation.dispatch mentioned in the review
rg "class GitHubInstallation\|def dispatch" src/sentry/integrations/github/ --type=py -B2 -A15

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Search for GitHubInstallation.dispatch and the webhook endpoint
rg "class GitHubInstallation\|def dispatch" src/sentry/integrations/github/ --type=py -B2 -A15

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Search for the Endpoint that handles the webhook POST
rg "@all_silo_endpoint\|class.*Endpoint" src/sentry/integrations/github/webhook.py -A20

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look at the end of the webhook.py file to find the Endpoint
tail -200 src/sentry/integrations/github/webhook.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 7801


🏁 Script executed:

#!/bin/bash
# Search for class names in webhook.py
rg "^class " src/sentry/integrations/github/webhook.py -n

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Check if GitHubIntegrationTest or its parent class mocks or disables signature validation
rg "is_valid_signature\|get_secret\|mock.*signature" tests/sentry/integrations/github/test_integration.py -B5 -A5

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Check the parent class IntegrationTestCase to see if it has special webhook handling
rg "class IntegrationTestCase" src/sentry/testutils/cases.py -A50

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 1839


🏁 Script executed:

#!/bin/bash
# Calculate what secret would produce the hardcoded signature
python3 << 'EOF'
import json
import hmac
import hashlib

# The INSTALLATION_EVENT_EXAMPLE modified with sender.login = "attacker"
event_json = """{
  "action": "created",
  "installation": {
    "id": 2,
    "account": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "access_tokens_url": "https://api.github.com/installations/2/access_tokens",
    "repositories_url": "https://api.github.com/installation/repositories"
  },
  "sender": {
    "login": "attacker",
    "id": 1,
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    "url": "https://api.github.com/users/octocat",
    "html_url": "https://github.com/octocat",
    "followers_url": "https://api.github.com/users/octocat/followers",
    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
    "organizations_url": "https://api.github.com/users/octocat/orgs",
    "repos_url": "https://api.github.com/users/octocat/repos",
    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
    "received_events_url": "https://api.github.com/users/octocat/received_events",
    "type": "User",
    "site_admin": false
  }
}"""

# The hardcoded signature
target_signature = "d184e6717f8bfbcc291ebc8c0756ee446c6c9486"

# Try common test secrets
test_secrets = [
    "",
    "test",
    "secret",
    "github-webhook-secret",
    "b3002c3e321d4b7880360d397db2ccfd",
]

payload = event_json.encode('utf-8')

for secret in test_secrets:
    sig = hmac.new(secret.encode('utf-8'), payload, hashlib.sha1).hexdigest()
    match = "✓" if sig == target_signature else " "
    print(f"[{match}] secret='{secret}': {sig}")
EOF

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 427


🏁 Script executed:

#!/bin/bash
# Check if the test mocks the webhook endpoint or signature validation
rg "mock\|patch\|@responses" tests/sentry/integrations/github/test_integration.py | grep -A3 -B3 "test_github_user_mismatch"

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look at the actual test method to see the full context
sed -n '398,454p' tests/sentry/integrations/github/test_integration.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 2464


🏁 Script executed:

#!/bin/bash
# Check if there are any test settings or middleware that disable webhook signature validation
rg "github.*webhook.*secret\|GITHUB.*SECRET" src/sentry/settings/ tests/settings.py -B2 -A2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 203


🏁 Script executed:

#!/antml:thinking>
Let me check if the test actually passes by looking for any test configuration that sets the webhook secret.
</thinking>
#!/bin/bash
# Check if the test uses pytest fixtures or settings that configure the webhook secret
grep -r "github-app.webhook-secret" tests/sentry/integrations/github/test_integration.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Check what happens when an empty secret is used - does signature validation pass or fail?
# Let's look at is_valid_signature more carefully
python3 << 'EOF'
import hmac
import hashlib
from django.utils.crypto import constant_time_compare

# Simulate what the endpoint does
event_json = """{
  "action": "created",
  "installation": {
    "id": 2,
    "account": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "access_tokens_url": "https://api.github.com/installations/2/access_tokens",
    "repositories_url": "https://api.github.com/installation/repositories"
  },
  "sender": {
    "login": "attacker",
    "id": 1,
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    "url": "https://api.github.com/users/octocat",
    "html_url": "https://github.com/octocat",
    "followers_url": "https://api.github.com/users/octocat/followers",
    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
    "organizations_url": "https://api.github.com/users/octocat/orgs",
    "repos_url": "https://api.github.com/users/octocat/repos",
    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
    "received_events_url": "https://api.github.com/users/octocat/received_events",
    "type": "User",
    "site_admin": false
  }
}"""

body = event_json.encode('utf-8')
secret = ""
signature = "d184e6717f8bfbcc291ebc8c0756ee446c6c9486"

# This is what is_valid_signature does
expected = hmac.new(key=secret.encode("utf-8"), msg=body, digestmod=hashlib.sha1).hexdigest()
print(f"Expected signature: {expected}")
print(f"Provided signature: {signature}")
print(f"Match: {expected == signature}")

# The constant_time_compare would return False
print(f"Validation would return: {expected == signature}")
EOF

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Check the test configuration
python3 << 'EOF'
import hmac
import hashlib

event_json = """{
  "action": "created",
  "installation": {
    "id": 2,
    "account": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "access_tokens_url": "https://api.github.com/installations/2/access_tokens",
    "repositories_url": "https://api.github.com/installation/repositories"
  },
  "sender": {
    "login": "attacker",
    "id": 1,
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    "url": "https://api.github.com/users/octocat",
    "html_url": "https://github.com/octocat",
    "followers_url": "https://api.github.com/users/octocat/followers",
    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
    "organizations_url": "https://api.github.com/users/octocat/orgs",
    "repos_url": "https://api.github.com/users/octocat/repos",
    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
    "received_events_url": "https://api.github.com/users/octocat/received_events",
    "type": "User",
    "site_admin": false
  }
}"""

body = event_json.encode('utf-8')
secret = ""
signature = "d184e6717f8bfbcc291ebc8c0756ee446c6c9486"

expected = hmac.new(key=secret.encode("utf-8"), msg=body, digestmod=hashlib.sha1).hexdigest()
print(f"Expected signature (empty secret): {expected}")
print(f"Provided signature in test:        {signature}")
print(f"Match: {expected == signature}")
EOF

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 249


🏁 Script executed:

#!/bin/bash
# Check settings files to see if there's a default secret for tests
find . -name "settings*.py" -o -name "conftest.py" | head -20

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 340


🏁 Script executed:

#!/bin/bash
# Look for the test runner configuration
rg "github-app" . --type=py | grep -i "option\|setting\|default" | head -20

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 2199


🏁 Script executed:

#!/bin/bash
# Check if GitHubIntegrationTest is using responses library to mock the webhook endpoint
rg "@responses.activate" tests/sentry/integrations/github/test_integration.py -B5 | grep -A5 "test_github_user_mismatch"

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/an/bash
# Check if the webhook endpoint is being mocked/patched in the test
rg "mock\|patch\|responses.add" tests/sentry/integrations/github/test_integration.py | grep -i "webhook\|endpoint\|signature" -B2 -A2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look for how responses library is configured in tests/conftest.py
head -100 tests/conftest.py

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 3634


The hardcoded webhook HMAC signature is incorrect and will cause the test to fail.

The test expects resp.status_code == 204, but the signature sha1=d184e6717f8bfbcc291ebc8c0756ee446c6c9486 does not match the HMAC-SHA1 of the webhook payload. Since GitHubIntegrationTest does not call options.set("github-app.webhook-secret", ...) (unlike other webhook tests), it uses the default empty secret. With an empty secret, the correct signature would be sha1=5edb0b1e7ce2ee0fabc7805215f2a1a87c73c2fd. The endpoint's signature validation will reject the request with HTTP 401 rather than processing it.

The signature must be computed correctly or the test webhook secret must be explicitly configured. If the secret changes in the future, the test will fail silently at the webhook validation layer before reaching the user mismatch check.

🤖 Prompt for AI Agents
In `@tests/sentry/integrations/github/test_integration.py` around lines 398 - 454,
The test test_github_user_mismatch fails because the hardcoded
HTTP_X_HUB_SIGNATURE value does not match the HMAC-SHA1 of the webhook payload
(INSTALLATION_EVENT_EXAMPLE) given the default empty webhook secret, causing the
webhook to be rejected before the user-mismatch logic runs; to fix, either set
the webhook secret explicitly via options.set("github-app.webhook-secret",
"<secret>") in the test setup and use the matching signature header, or compute
the correct HMAC-SHA1 for INSTALLATION_EVENT_EXAMPLE with the secret used and
replace the HTTP_X_HUB_SIGNATURE header in the client.post call so the signature
validation in the GitHub webhook endpoint passes and the test exercises
GitHubIntegrationTest::test_github_user_mismatch logic.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant